// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 1996-2015, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */

package com.ibm.icu.dev.test.normalizer;

import com.ibm.icu.dev.test.CoreTestFmwk;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.Normalizer;
import com.ibm.icu.text.UTF16;
import com.ibm.icu.text.UnicodeSet;
import java.io.BufferedReader;
import java.io.IOException;
import java.text.StringCharacterIterator;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ConformanceTest extends CoreTestFmwk {

    Normalizer normalizer;

    public ConformanceTest() {
        // Doesn't matter what the string and mode are; we'll change
        // them later as needed.
        normalizer = new Normalizer("", Normalizer.NFC, 0);
    }

    // more interesting conformance test cases, not in the unicode.org NormalizationTest.txt
    static String[] moreCases = {
        // Markus 2001aug30
        "0061 0332 0308;00E4 0332;0061 0332 0308;00E4 0332;0061 0332 0308; # Markus 0",

        // Markus 2001oct26 - test edge case for iteration: U+0f73.cc==0 but
        // decomposition.lead.cc==129
        "0061 0301 0F73;00E1 0F71 0F72;0061 0F71 0F72 0301;00E1 0F71 0F72;0061 0F71 0F72 0301; # Markus 1"
    };

    /**
     * Test the conformance of Normalizer to
     * https://www.unicode.org/reports/tr15/conformance/Draft-TestSuite.txt.*
     * http://www.unicode.org/Public/UNIDATA/NormalizationTest.txt This file must be located at the
     * path specified as TEST_SUITE_FILE.
     */
    @Test
    public void TestConformance() throws Exception {
        runConformance("unicode/NormalizationTest.txt", 0);
    }

    @Test
    public void TestConformance_3_2() throws Exception {
        runConformance("unicode/NormalizationTest-3.2.0.txt", Normalizer.UNICODE_3_2);
    }

    public void runConformance(String fileName, int options) throws Exception {
        String line = null;
        String[] fields = new String[5];
        StringBuffer buf = new StringBuffer();
        int passCount = 0;
        int failCount = 0;
        UnicodeSet other = new UnicodeSet(0, 0x10ffff);
        int c = 0;
        BufferedReader input = null;
        try {
            input = TestUtil.getUtf8DataReader(fileName);
            for (int count = 0; ; ++count) {
                line = input.readLine();
                if (line == null) {
                    // read the extra test cases
                    if (count > moreCases.length) {
                        count = 0;
                    } else if (count == moreCases.length) {
                        // all done
                        break;
                    }
                    line = moreCases[count++];
                }
                if (line.length() == 0) continue;

                // Expect 5 columns of this format:
                // 1E0C;1E0C;0044 0323;1E0C;0044 0323; # <comments>

                // Skip comments
                if (line.charAt(0) == '#' || line.charAt(0) == '@') continue;

                // Parse out the fields
                hexsplit(line, ';', fields, buf);

                // Remove a single code point from the "other" UnicodeSet
                if (fields[0].length() == UTF16.moveCodePointOffset(fields[0], 0, 1)) {
                    c = UTF16.charAt(fields[0], 0);
                    if (0xac20 <= c && c <= 0xd73f) {
                        // not an exhaustive test run: skip most Hangul syllables
                        if (c == 0xac20) {
                            other.remove(0xac20, 0xd73f);
                        }
                        continue;
                    }
                    other.remove(c);
                }
                if (checkConformance(fields, line, options)) {
                    ++passCount;
                } else {
                    ++failCount;
                }
                if ((count % 1000) == 999) {
                    logln("Line " + (count + 1));
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            throw new IllegalArgumentException(
                    "Couldn't read file "
                            + ex.getClass().getName()
                            + " "
                            + ex.getMessage()
                            + " line = "
                            + line);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException ignored) {
                }
            }
        }

        if (failCount != 0) {
            errln("Total: " + failCount + " lines failed, " + passCount + " lines passed");
        } else {
            logln("Total: " + passCount + " lines passed");
        }
    }

    /**
     * Verify the conformance of the given line of the Unicode normalization (UTR 15) test suite
     * file. For each line, there are five columns, corresponding to field[0]..field[4].
     *
     * <p>The following invariants must be true for all conformant implementations c2 == NFC(c1) ==
     * NFC(c2) == NFC(c3) c3 == NFD(c1) == NFD(c2) == NFD(c3) c4 == NFKC(c1) == NFKC(c2) == NFKC(c3)
     * == NFKC(c4) == NFKC(c5) c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
     *
     * @param field the 5 columns
     * @param line the source line from the test suite file
     * @return true if the test passes
     */
    private boolean checkConformance(String[] field, String line, int options) throws Exception {
        boolean pass = true;
        StringBuffer buf = new StringBuffer(); // scratch
        String out, fcd;
        int i = 0;
        for (i = 0; i < 5; ++i) {
            int fieldNum = i + 1;
            if (i < 3) {
                pass &= checkNorm(Normalizer.NFC, options, field[i], field[1], fieldNum);
                pass &= checkNorm(Normalizer.NFD, options, field[i], field[2], fieldNum);
            }
            pass &= checkNorm(Normalizer.NFKC, options, field[i], field[3], fieldNum);
            pass &= checkNorm(Normalizer.NFKD, options, field[i], field[4], fieldNum);
            cross(field[4] /*NFKD String*/, field[3] /*NFKC String*/, Normalizer.NFKC);
            cross(field[3] /*NFKC String*/, field[4] /*NFKD String*/, Normalizer.NFKD);
        }
        compare(field[1], field[2]);
        compare(field[0], field[1]);
        compare(field[0], field[2]);
        // test quick checks
        if (Normalizer.NO == Normalizer.quickCheck(field[1], Normalizer.NFC, options)) {
            errln("Normalizer error: quickCheck(NFC(s), Normalizer.NFC) is Normalizer.NO");
            pass = false;
        }
        if (Normalizer.NO == Normalizer.quickCheck(field[2], Normalizer.NFD, options)) {
            errln("Normalizer error: quickCheck(NFD(s), Normalizer.NFD) is Normalizer.NO");
            pass = false;
        }
        if (Normalizer.NO == Normalizer.quickCheck(field[3], Normalizer.NFKC, options)) {
            errln("Normalizer error: quickCheck(NFKC(s), Normalizer.NFKC) is Normalizer.NO");
            pass = false;
        }
        if (Normalizer.NO == Normalizer.quickCheck(field[4], Normalizer.NFKD, options)) {
            errln("Normalizer error: quickCheck(NFKD(s), Normalizer.NFKD) is Normalizer.NO");
            pass = false;
        }

        if (!Normalizer.isNormalized(field[1], Normalizer.NFC, options)) {
            errln("Normalizer error: isNormalized(NFC(s), Normalizer.NFC) is false");
            pass = false;
        }
        if (!field[0].equals(field[1])
                && Normalizer.isNormalized(field[0], Normalizer.NFC, options)) {
            errln("Normalizer error: isNormalized(s, Normalizer.NFC) is true");
            pass = false;
        }
        if (!Normalizer.isNormalized(field[3], Normalizer.NFKC, options)) {
            errln("Normalizer error: isNormalized(NFKC(s), Normalizer.NFKC) is false");
            pass = false;
        }
        if (!field[0].equals(field[3])
                && Normalizer.isNormalized(field[0], Normalizer.NFKC, options)) {
            errln("Normalizer error: isNormalized(s, Normalizer.NFKC) is true");
            pass = false;
        }
        // test api that takes a char[]
        if (!Normalizer.isNormalized(
                field[1].toCharArray(), 0, field[1].length(), Normalizer.NFC, options)) {
            errln("Normalizer error: isNormalized(NFC(s), Normalizer.NFC) is false");
            pass = false;
        }
        // test api that takes a codepoint
        if (!Normalizer.isNormalized(UTF16.charAt(field[1], 0), Normalizer.NFC, options)) {
            errln("Normalizer error: isNormalized(NFC(s), Normalizer.NFC) is false");
            pass = false;
        }
        // test FCD quick check and "makeFCD"
        fcd = Normalizer.normalize(field[0], Normalizer.FCD);
        if (Normalizer.NO == Normalizer.quickCheck(fcd, Normalizer.FCD, options)) {
            errln("Normalizer error: quickCheck(FCD(s), Normalizer.FCD) is Normalizer.NO");
            pass = false;
        }
        // check FCD return length
        {
            char[] fcd2 = new char[fcd.length() * 2];
            char[] src = field[0].toCharArray();
            int fcdLen =
                    Normalizer.normalize(
                            src, 0, src.length, fcd2, fcd.length(), fcd2.length, Normalizer.FCD, 0);
            if (fcdLen != fcd.length()) {
                errln("makeFCD did not return the correct length");
            }
        }
        if (Normalizer.NO == Normalizer.quickCheck(fcd, Normalizer.FCD, options)) {
            errln("Normalizer error: quickCheck(FCD(s), Normalizer.FCD) is Normalizer.NO");
            pass = false;
        }
        if (Normalizer.NO == Normalizer.quickCheck(field[2], Normalizer.FCD, options)) {
            errln("Normalizer error: quickCheck(NFD(s), Normalizer.FCD) is Normalizer.NO");
            pass = false;
        }

        if (Normalizer.NO == Normalizer.quickCheck(field[4], Normalizer.FCD, options)) {
            errln("Normalizer error: quickCheck(NFKD(s), Normalizer.FCD) is Normalizer.NO");
            pass = false;
        }

        out =
                iterativeNorm(
                        new StringCharacterIterator(field[0]), Normalizer.FCD, buf, +1, options);
        out =
                iterativeNorm(
                        new StringCharacterIterator(field[0]), Normalizer.FCD, buf, -1, options);

        out =
                iterativeNorm(
                        new StringCharacterIterator(field[2]), Normalizer.FCD, buf, +1, options);
        out =
                iterativeNorm(
                        new StringCharacterIterator(field[2]), Normalizer.FCD, buf, -1, options);

        out =
                iterativeNorm(
                        new StringCharacterIterator(field[4]), Normalizer.FCD, buf, +1, options);
        out =
                iterativeNorm(
                        new StringCharacterIterator(field[4]), Normalizer.FCD, buf, -1, options);

        out = Normalizer.normalize(fcd, Normalizer.NFD);
        if (!out.equals(field[2])) {
            errln("Normalizer error: NFD(FCD(s))!=NFD(s)");
            pass = false;
        }
        if (!pass) {
            errln("FAIL: " + line);
        }
        if (field[0] != field[2]) {
            // two strings that are canonically equivalent must test
            // equal under a canonical caseless match
            // see UAX #21 Case Mappings and Jitterbug 2021 and
            // Unicode Technical Committee meeting consensus 92-C31
            int rc;
            if ((rc =
                            Normalizer.compare(
                                    field[0],
                                    field[2],
                                    (options << Normalizer.COMPARE_NORM_OPTIONS_SHIFT)
                                            | Normalizer.COMPARE_IGNORE_CASE))
                    != 0) {
                errln(
                        "Normalizer.compare(original, NFD, case-insensitive) returned "
                                + rc
                                + " instead of 0 for equal");
                pass = false;
            }
        }

        return pass;
    }

    private static int getModeNumber(Normalizer.Mode mode) {
        if (mode == Normalizer.NFD) {
            return 0;
        }
        if (mode == Normalizer.NFKD) {
            return 1;
        }
        if (mode == Normalizer.NFC) {
            return 2;
        }
        if (mode == Normalizer.NFKC) {
            return 3;
        }
        return -1;
    }

    private static final String[] kModeStrings = {"D", "KD", "C", "KC"};
    private static final String[] kMessages = {
        "c3!=D(c%d)", "c5!=KC(c%d)", "c2!=C(c%d)", "c4!=KC(c%d)"
    };

    boolean checkNorm(
            Normalizer.Mode mode,
            int options, // Normalizer2 norm2,
            String s,
            String exp,
            int field)
            throws Exception {
        String modeString = kModeStrings[getModeNumber(mode)];
        String msg = String.format(kMessages[getModeNumber(mode)], field);
        StringBuffer buf = new StringBuffer();
        String out = Normalizer.normalize(s, mode, options);
        if (!assertEqual(modeString, "", s, out, exp, msg)) {
            return false;
        }

        out = iterativeNorm(s, mode, buf, +1, options);
        if (!assertEqual(modeString, "(+1)", s, out, exp, msg)) {
            return false;
        }

        out = iterativeNorm(s, mode, buf, -1, options);
        if (!assertEqual(modeString, "(-1)", s, out, exp, msg)) {
            return false;
        }

        out = iterativeNorm(new StringCharacterIterator(s), mode, buf, +1, options);
        if (!assertEqual(modeString, "(+1)", s, out, exp, msg)) {
            return false;
        }

        out = iterativeNorm(new StringCharacterIterator(s), mode, buf, -1, options);
        if (!assertEqual(modeString, "(-1)", s, out, exp, msg)) {
            return false;
        }

        return true;
    }

    // two strings that are canonically equivalent must test
    // equal under a canonical caseless match
    // see UAX #21 Case Mappings and Jitterbug 2021 and
    // Unicode Technical Committee meeting consensus 92-C31
    private void compare(String s1, String s2) {
        if (s1.length() == 1 && s2.length() == 1) {
            if (Normalizer.compare(
                            UTF16.charAt(s1, 0),
                            UTF16.charAt(s2, 0),
                            Normalizer.COMPARE_IGNORE_CASE)
                    != 0) {
                errln(
                        "Normalizer.compare(int,int) failed for s1: "
                                + Utility.hex(s1)
                                + " s2: "
                                + Utility.hex(s2));
            }
        }
        if (s1.length() == 1 && s2.length() > 1) {
            if (Normalizer.compare(UTF16.charAt(s1, 0), s2, Normalizer.COMPARE_IGNORE_CASE) != 0) {
                errln(
                        "Normalizer.compare(int,String) failed for s1: "
                                + Utility.hex(s1)
                                + " s2: "
                                + Utility.hex(s2));
            }
        }
        if (s1.length() > 1 && s2.length() > 1) {
            // TODO: Re-enable this tests after UTC fixes UAX 21
            if (Normalizer.compare(
                            s1.toCharArray(), s2.toCharArray(), Normalizer.COMPARE_IGNORE_CASE)
                    != 0) {
                errln(
                        "Normalizer.compare(char[],char[]) failed for s1: "
                                + Utility.hex(s1)
                                + " s2: "
                                + Utility.hex(s2));
            }
        }
    }

    private void cross(String s1, String s2, Normalizer.Mode mode) {
        String result = Normalizer.normalize(s1, mode);
        if (!result.equals(s2)) {
            errln("cross test failed s1: " + Utility.hex(s1) + " s2: " + Utility.hex(s2));
        }
    }

    /**
     * Do a normalization using the iterative API in the given direction.
     *
     * @param buf scratch buffer
     * @param dir either +1 or -1
     */
    private String iterativeNorm(
            String str, Normalizer.Mode mode, StringBuffer buf, int dir, int options)
            throws Exception {
        normalizer.setText(str);
        normalizer.setMode(mode);
        buf.setLength(0);
        normalizer.setOption(-1, false); // reset all options
        normalizer.setOption(options, true); // set desired options

        int ch;
        if (dir > 0) {
            for (ch = normalizer.first(); ch != Normalizer.DONE; ch = normalizer.next()) {
                buf.append(UTF16.valueOf(ch));
            }
        } else {
            for (ch = normalizer.last(); ch != Normalizer.DONE; ch = normalizer.previous()) {
                buf.insert(0, UTF16.valueOf(ch));
            }
        }
        return buf.toString();
    }

    /**
     * Do a normalization using the iterative API in the given direction.
     *
     * @param str a Java StringCharacterIterator
     * @param buf scratch buffer
     * @param dir either +1 or -1
     */
    private String iterativeNorm(
            StringCharacterIterator str,
            Normalizer.Mode mode,
            StringBuffer buf,
            int dir,
            int options)
            throws Exception {
        normalizer.setText(str);
        normalizer.setMode(mode);
        buf.setLength(0);
        normalizer.setOption(-1, false); // reset all options
        normalizer.setOption(options, true); // set desired options

        int ch;
        if (dir > 0) {
            for (ch = normalizer.first(); ch != Normalizer.DONE; ch = normalizer.next()) {
                buf.append(UTF16.valueOf(ch));
            }
        } else {
            for (ch = normalizer.last(); ch != Normalizer.DONE; ch = normalizer.previous()) {
                buf.insert(0, UTF16.valueOf(ch));
            }
        }
        return buf.toString();
    }

    /**
     * @param op name of normalization form, e.g., "KC"
     * @param op2 name of test case variant, e.g., "(-1)"
     * @param s string being normalized
     * @param got value received
     * @param exp expected value
     * @param msg description of this test
     * @returns true if got == exp
     */
    private boolean assertEqual(
            String op, String op2, String s, String got, String exp, String msg) {
        if (exp.equals(got)) {
            return true;
        }
        errln(
                ("      " + msg + ": " + op + op2 + '(' + s + ")=" + hex(got) + ", exp. "
                        + hex(exp)));
        return false;
    }

    /**
     * Split a string into pieces based on the given delimiter character. Then, parse the resultant
     * fields from hex into characters. That is, "0040 0400;0C00;0899" -> new String[] {
     * "\u0040\u0400", "\u0C00", "\u0899" }. The output is assumed to be of the proper length
     * already, and exactly output.length fields are parsed. If there are too few an exception is
     * thrown. If there are too many the extras are ignored.
     *
     * @param buf scratch buffer
     */
    private static void hexsplit(String s, char delimiter, String[] output, StringBuffer buf) {
        int i;
        int pos = 0;
        for (i = 0; i < output.length; ++i) {
            int delim = s.indexOf(delimiter, pos);
            if (delim < 0) {
                throw new IllegalArgumentException("Missing field in " + s);
            }
            // Our field is from pos..delim-1.
            buf.setLength(0);

            String toHex = s.substring(pos, delim);
            pos = delim;
            int index = 0;
            int len = toHex.length();
            while (index < len) {
                if (toHex.charAt(index) == ' ') {
                    index++;
                } else {
                    int spacePos = toHex.indexOf(' ', index);
                    if (spacePos == -1) {
                        appendInt(buf, toHex.substring(index, len), s);
                        spacePos = len;
                    } else {
                        appendInt(buf, toHex.substring(index, spacePos), s);
                    }
                    index = spacePos + 1;
                }
            }

            if (buf.length() < 1) {
                throw new IllegalArgumentException("Empty field " + i + " in " + s);
            }
            output[i] = buf.toString();
            ++pos; // Skip over delim
        }
    }

    public static void appendInt(StringBuffer buf, String strToHex, String s) {
        int hex = Integer.parseInt(strToHex, 16);
        if (hex < 0) {
            throw new IllegalArgumentException("Out of range hex " + hex + " in " + s);
        } else if (hex > 0xFFFF) {
            buf.append((char) ((hex >> 10) + 0xd7c0));
            buf.append((char) ((hex & 0x3ff) | 0xdc00));
        } else {
            buf.append((char) hex);
        }
    }

    // Specific tests for debugging.  These are generally failures
    // taken from the conformance file, but culled out to make
    // debugging easier.  These can be eliminated without affecting
    // coverage.
    @Ignore
    @Test
    public void _hideTestCase6(/*int options*/ ) throws Exception {
        _testOneLine("0385;0385;00A8 0301;0020 0308 0301;0020 0308 0301;", /*options*/ 0);
    }

    private void _testOneLine(String line, int options) throws Exception {
        String[] fields = new String[5];
        StringBuffer buf = new StringBuffer();
        // Parse out the fields
        hexsplit(line, ';', fields, buf);
        checkConformance(fields, line, options);
    }
}
